package com.example.shiroplusdemo.filter;


import com.alibaba.fastjson.JSON;
import com.example.shiroplusdemo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * @author ：Anitaxiaofeng
 * @description：1.读取当前登录用户名，获取在缓存中的sessionId队列
 *              2.判断队列的长度，大于最大登录限制的时候，按提出规则将之前的sessionId中的session域中存入kickout：true
 *              并更新队列缓存
 *              3.判断当前登录的session域中kickout如果为true，将其做提出处理，然后在重定向到踢出登录提示页面
 * @create 2018-01-10
 **/
@Slf4j
public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl;//踢出后到的地址
    private boolean kickoutAfter = false; //踢出以前登录的用户或者是后来登录的用户，默认踢出之前登录的用户
    private int maxSession = 1; //同一个账号最大会话数，默认1
    private SessionManager sessionManager;
    private Cache<String,Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl){
        this.kickoutUrl = kickoutUrl;
    }
    public void setKickoutAfter(boolean kickoutAfter){
        this.kickoutAfter = kickoutAfter;
    }
    public void setMaxSession(int maxSession){
        this.maxSession = maxSession;
    }
    public void setSessionManager(SessionManager sessionManager){
        this.sessionManager = sessionManager;
    }

    /**
     * 设置Cache的key的前缀
     * @param cacheManager
     */
    public void setCacheManager(CacheManager cacheManager){
        this.cache = cacheManager.getCache("shiro_redis_test");
    }

    /**
     * 表示是否允许访问；mappedValue就是[urls]配置中拦截器参数部分，如果允许访问返回true，否则false；
     * @param servletRequest
     * @param servletResponse
     * @param
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
        return false; //表示不允许访问
    }

    /**
     * 表示当访问拒绝时是否已经处理了；如果返回true表示需要其他filter继续处理；如果返回false表示该拦截器实例已经处理了，将直接返回即可
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        if(!subject.isAuthenticated()&&!subject.isRemembered()){
            //如果没有登录，直接进行之后的流程
            return true;
        }
        //登录之后再进行的处理session
        Session session = subject.getSession();
        User user = (User) subject.getPrincipal();
        String nickname = user.getNickname();
        Serializable sessionId = session.getId();

        //读取缓存，没有就存入
        Deque<Serializable> deque = cache.get(nickname);
        //如果此用户没有sessionId队列，也就是还没有登录过，缓存中没有就new一个空队列
        if(deque==null){
            deque = new LinkedList<Serializable>();
        }

        //如果此队列没有此sessionId，且用户没有被踢出，放入队列
        if(!deque.contains(sessionId)&&session.getAttribute("kickout")==null){
            deque.push(sessionId);
            cache.put(nickname,deque);
        }

        //如果队列里的sessionId数超过最大会话数，开始踢人
        while(deque.size()>maxSession){
            Serializable kickoutSessionId = null;
            if(kickoutAfter){//如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后在更新一下缓存队列
                cache.put(nickname,deque);
            }else{
                kickoutSessionId = deque.removeLast();
                cache.put(nickname,deque);
            }
            //获取被踢出的sessionId的session对象
            Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
            if(kickoutSession!=null){
                kickoutSession.setAttribute("kickout",true);
            }
        }

        //如果被踢出了，直接退出，重定向到踢出后的地址
        if(session.getAttribute("kickout")!=null&&(Boolean)session.getAttribute("kickout")==true){
            subject.logout();

        saveRequest(servletRequest);
        Map<String,String> resultMap = new HashMap<>();
        //判断是不是Ajax请求
        if("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest)servletRequest).getHeader("X-Requested-With"))){
            resultMap.put("user_status","300");
            resultMap.put("message","您已经在其他地方登录，请重新登录");
            //输出json
            out(servletResponse,resultMap);
        } else{
            //重定向
            WebUtils.issueRedirect(servletRequest,servletResponse,kickoutUrl);
        }
        return false;
        }
        return true;
    }

    private void out(ServletResponse response,Map<String,String> resultMap) throws IOException{

        response.setCharacterEncoding("UTF-8");

        try{
            PrintWriter out = response.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        }catch(Exception e){
            log.info("KickoutSessionFilter.class 输出JSON异常");
        }

    }
}
